/**
 * @file trayinfo.c
 * Provides periodical information for the system tray of dwm, wmii, awesome...
 */

/* Created by Laszlo Ashin <kodest@gmail.com> */

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <ltdl.h>
#include <sys/socket.h>

#include "trayinfo.h"
#include "utils.h"
#include "sendq.h"

#ifdef HAVE_LIBSSL
# include <openssl/ssl.h>
# include <openssl/err.h>
# include <openssl/engine.h>
# include <openssl/conf.h>
#endif /* HAVE_LIBSSL */

/* #include "compiler.h" */

/** Default delay between updates. */
static double delay = 1.0f;

/** Configuration file path. */
static char config_fname[128] = "";

/** Linked list item containing a source. */
typedef struct source_item_s {

	/** Linking pointer. */
	/*@null@*/ struct source_item_s *next;

	/** The source structure */
	source_t source;

	/**
	 * The source can define a network connection by implementing the
	 * open() callback. In this case trayinfo stores the file
	 * descriptor of the opened socket in this variable.
	 * @see open
	 */
	int fd;

	/** Time of the last call of the open() callback. */
	time_t last_retry;

	/** Send queue of the source*/
	sendq_t sendq;

} source_item_t;

/** Head of linked list containing the sources. */
/*@null@*/ /*@owned@*/ static source_item_t *first_source = NULL;

/** Tells us whether a config reload is requested. */
static int reload_requested = 0;

/** Tells us whether quit is requested. */
static int quit_requested = 0;

/** Linked list item containing a configuration option handler. */
typedef struct config_item_s {

	/** Linking pointer. */
	/*@null@*/ struct config_item_s *next;

	/** Name of the configuration option. */
	/*@dependent@*/ char name[CONFIG_ITEM_NAME_SIZE];

	/** Function pointer to the handler of this option. */
	/*@dependent@*/ config_handler_t handler;

} config_item_t;

/** Head of linked list containing configuration option handlers. */
/*@null@*/ /*@owned@*/ static config_item_t *first_config_item = NULL;

/** Linked list item containing a loaded module. */
typedef struct module_s {

	/** Linking pointer. */
	/*@null@*/ struct module_s *next;

	/** Name of the module. */
	/*@dependent@*/ char name[MODULE_NAME_SIZE];

	/** Function pointer to the init function of the module. */
	module_init_func_t init;

	/** Handle for the module. */
	lt_dlhandle handle;

} module_t;

typedef struct argument_command_s {
	char cmd;
	const char *command;
	int nparams;
	void (*handler)(char *argv[]);
	const char *help;
} argument_command_t;

/** Head of linked list containing loaded modules. */
/*@null@*/ /*@owned@*/ static module_t *first_module = NULL;

void
request_quit(void)
{
	quit_requested = !0;
}

/**
 * Signal handler.
 */
static void
sigaction_handler(int sig_num, /*@unused@*/ siginfo_t *info, /*@unused@*/ void *p)
{
	switch (sig_num) {
	case SIGHUP:
	case SIGUSR1:
		reload_requested = !0;
		break;
	case SIGTERM:
	case SIGINT:
		request_quit();
		break;
	}
}

/**
 * Sets the delay between updates.
 * @param s String representation of a floating point value.
 * @return Zero on success.
 */
static int
config_delay(const char *s)
{
	double d;

	if (sscanf(s, "%lf", &d) != 1) {
		return 1;
	}
	delay = d;
	return 0;
}

ssize_t
source_send(source_handle_t *sh, const void *buf, size_t len)
{
	source_item_t *si = (source_item_t *)sh;

	sendq_put(&(si->sendq), buf, len);

	return len;
}

/**
 * Loads a module.
 * @param name Name of the module to load.
 * @return Zero on success.
 */
static int
add_module(/*@dependent@*/ const char *name)
{
	lt_dlhandle handle;
	union {
		lt_ptr ptr;
		module_init_func_t func;
	} init;

	handle = lt_dlopenext(name);
	if (handle == NULL) {
		show_error("failed to load module %s - %s",
			name, lt_dlerror());
		return !0;
	}

	init.ptr = lt_dlsym(handle, "init_module");
	if (init.ptr == NULL) {
		show_error("failed to locate init function of module %s - %s",
			name, lt_dlerror());
		(void)lt_dlclose(handle);
		return !0;
	}

	if (init.func(MODULE_PHASE_INIT) != 0) {
		show_error("failed to initialize module %s", name);
		(void)lt_dlclose(handle);
		return !0;
	} else {
		module_t *module = NEW(module_t);

		if (module == NULL) {
			(void)lt_dlclose(handle);
			return !0;
		}

		strncpy(module->name, name, MODULE_NAME_SIZE - 1);
		module->name[MODULE_NAME_SIZE - 1] = '\0';
		module->init = init.func;
		module->handle = handle;
		module->next = first_module;
		first_module = module;
		return 0;
	}
}

int
register_config_handler(const char *name, config_handler_t handler)
{
	config_item_t *item = NEW(config_item_t);
	if (!item) {
		return !0;
	}

	item->next = first_config_item;
	strncpy(item->name, name, CONFIG_ITEM_NAME_SIZE - 1);
	item->name[CONFIG_ITEM_NAME_SIZE - 1] = '\0';
	item->handler = handler;
	first_config_item = item;
	return 0;
}

int
forget_config_handler(const char *name, config_handler_t handler)
{
	int err = !0;
	config_item_t *item = first_config_item;
	config_item_t **nextp = &(first_config_item);

	while (item != NULL) {
		if (strcmp(name, item->name) == 0 && handler == item->handler) {
			*nextp = item->next;
			free(item);
			err = 0; /* sign that we have deleted an item */
		} else {
			nextp = &(item->next);
		}

		item = *nextp;
	}

	if (err != 0) {
		show_error("Configuration option handler not found (%s)",
			name);
	}
	return err;
}

source_handle_t *
register_source(source_t *source)
{
	source_item_t *si;

	if (source == NULL || source->name == NULL) {
		show_error("Invalid source.");
		return NULL;
	}

	si = NEW(source_item_t);
	if (si == NULL) {
		return NULL;
	}

	memcpy(&(si->source), source, sizeof(source_t));

	init_sendq(&(si->sendq));

	if (si->source.wait_retry == 0) {
		si->source.wait_retry = WAIT_RETRY;
	}

	/*@-mustfreeonly@*/ si->next = first_source; /*@+mustfreeonly@*/
	first_source = si;

	if (si->source.init != NULL &&
	    si->source.init((source_handle_t *)si) != 0) {
		first_source = si->next;
		show_error("failed to initialize source %s", si->source.name);
		free(si);
		return NULL;
	}

	if (si->source.open != NULL) {
		si->fd = si->source.open();
		si->last_retry = time(NULL);
	} else {
		si->fd = 0;
		si->last_retry = 0;
	}

	return si;
}

int
forget_source(source_handle_t *sh)
{
	source_item_t *si = first_source;
	source_item_t **nextp = &(first_source);

	while (si != NULL) {
		if (si == sh) {
			*nextp = si->next;

			if (si->source.close) {
				si->source.close(si->fd);
				si->fd = 0;
			}
			if (si->source.done) {
				si->source.done();
			}

			free_sendq(&(si->sendq));

			free(si);
			return 0;
		}
		nextp = &(si->next);
		si = *nextp;
	}

	show_error("forget_source(): source not found.");
	return !0;
}

/**
 * Executes the specified configuration option handler with the given params.
 * @param name Name of the configuration option.
 * @param param Parameters of the configuration option.
 * @return Return value of the executed handler or -1 on error.
 */
static int
exec_config_handler(const char *name, const char *param)
{
	config_item_t *item = first_config_item;

	while (item != NULL) {
		if (strcmp(item->name, name) == 0) {
			return item->handler(param);
		}

		item = item->next;
	}

	return -1;
}

/**
 * Loads the configuration file.
 * @param fname Name of the configuration file.
 */
static void
load_config(const char *fname)
{
	char buf[80];
	FILE *f;
	int i;

	f = fopen(fname, "r");
	if (!f) {
		show_error("Unable to open configuration file \"%s\".", fname);
		return;
	}
	i = 0;
	while (fgets(buf, (int)sizeof(buf), f) == buf) {
		char *s, *t;
		int ret;

		++i;
		for (s = buf; *s != '\0' && *s <= ' '; ++s);
		if (*s == '\0' || *s == '#') continue;

		for (t = s + strlen(s) - 1; t >= s && *t <= ' '; *t-- = '\0');
		t = s;
		while (*s != '\0' && *s > ' ') ++s;
		if (*s != '\0') *s++ = '\0';
		if (*s == '"') {
			char *u = s + strlen(s) - 1;
			++s;
			if (*u != '"') {
				show_error("%s:%d: quotes must be closed",
					fname, i);
				continue;
			}
			*u = '\0';
		}

		ret = exec_config_handler(t, s);
		if (ret > 0) {
			show_error("%s:%d: a mistake was found",
				fname, i);
		} else if (ret < 0) {
			show_error("%s:%d: no such cfg action (%s)",
				fname, i, t);
		}
	}
	(void)fclose(f);
}

/**
 * Initialize the sources by loading configuration file.
 */
static void
open_sources(void)
{
	if (config_fname[0] == '\0') {
		char *home;
		size_t size;

		home = getenv("HOME");
		if (home == NULL) {
			return;
		}

		size = strlen(home) + strlen(CONFIG_FNAME) + 1;

		if (size > sizeof(config_fname)) {
			show_error("Sorry, configuration file path is "
				"too long. (max %d chars)",
				sizeof(config_fname));
			return;
		}

		strcpy(config_fname, home);
		strcat(config_fname, CONFIG_FNAME);
	}
	load_config(config_fname);
}

/**
 * Close all sources by unloading modules.
 */
static void
close_sources(void)
{
	while (first_module) {
		module_t *module = first_module;
		first_module = module->next;

		if (module->init(MODULE_PHASE_EXIT) != 0) {
			show_error("Error shuting down module %s.",
				module->name);
		}
		if (lt_dlclose(module->handle) != 0) {
			show_error("Error closing module %s.",
				module->name);
		}

		free(module);
	}
}

static void argcmd_help(char *argv[]);
static void argcmd_version(char *argv[]);
static void argcmd_config_file(char *argv[]);

static argument_command_t argument_commands[] = {
	{ 'h', "help", 0, argcmd_help, "Displays this help." },
	{ 'v', "version", 0, argcmd_version, "Displays version information." },
	{ 'f', "config-file", 1, argcmd_config_file,
		"Specifies an alternative configuration file."},
	{ '\0', "", 0, NULL, "" }
};

static void
argcmd_help(char *argv[])
{
	argument_command_t *ac = argument_commands;
	const char help_string[] = 
		"TrayInfo version " VERSION "\n"
		"\n"
		"Usage: trayinfo [-hv] [-f config]\n";

	puts(help_string);

	while (ac->cmd != '\0') {
		printf("  -%c\t%s\n", ac->cmd, ac->help);
		if (ac->command != NULL && ac->command[0] != '\0') {
			printf("\t--%s\n", ac->command);
		}
		++ac;
	}
	exit(0);
}

static void
argcmd_version(char *argv[])
{
	puts("TrayInfo version " VERSION);
	exit(0);
}

static void
argcmd_config_file(char *argv[])
{
	if (strlen(argv[0]) >= sizeof(config_fname)) {
		show_error("Sorry, configuration file path is "
			"too long. (max %d chars)",
			sizeof(config_fname));
		return;
	}
	strncpy(config_fname, argv[0], sizeof(config_fname) - 1);
	config_fname[sizeof(config_fname) - 1] = '\0';
}

/**
 * Process command line arguments.
 * @param argc Number of arguments.
 * @param argv Array of argument strings.
 */
static void
process_args(int argc, char *argv[])
{
	int cca = 1; /* current command argument */
	int cc = -1; /* current command */
	int np = 2; /* next parameter */
	argument_command_t *ac;

	while (cca < argc) {
		if (cc < 0) {
			if (argv[cca] == NULL || argv[cca][0] != '-' ||
			    argv[cca][1] == '\0') {
				show_error("Argument #%d must be command.",
					cca);
				return;
			}
			cc = 0;
		}
		++cc;
		if (argv[cca][cc] == '\0') {
			cca = np;
			++np;
			cc = -1;
			continue;
		}
		ac = argument_commands;
		if (cc == 1 && argv[cca][cc] == '-') {
			while (ac->cmd != '\0' && ac->command != NULL &&
			    ac->command[0] != '\0' &&
			    strcmp(ac->command, argv[cca] + 2) != 0) {
				++ac;
			}
			if (ac->cmd == '\0') {
				show_error("Unknown argument command (%s)",
					argv[cca] + 2);
				return;
			}
			cc = strlen(argv[cca]) - 1;
		} else {
			while (ac->cmd != '\0' && ac->cmd != argv[cca][cc]) {
				++ac;
			}
			if (ac->cmd == '\0') {
				show_error("Unknown argument command [%c]",
					argv[cca][cc]);
				return;
			}
		}
		if (argc < np + ac->nparams) {
			show_error("Too few parameters for command \"%c\".",
				argv[cca][cc]);
			show_error("Needs %d, got %d.",
				ac->nparams, argc - np);
			return;
		}
		ac->handler(argv + np);
		np += ac->nparams;
	}
}

/**
 * Register signal handler.
 */
static void
register_signal_handler(void)
{
	struct sigaction sa;

	if (sigemptyset(&sa.sa_mask) == 0) {
		sa.sa_flags = SA_SIGINFO;
		sa.sa_sigaction = sigaction_handler;
		(void)sigaction(SIGHUP, &sa, NULL);
		(void)sigaction(SIGUSR1, &sa, NULL);
		(void)sigaction(SIGTERM, &sa, NULL);
		(void)sigaction(SIGINT, &sa, NULL);
	}
}

/**
 * Initialize the application.
 */
static void
initialize(int argc, char *argv[])
{
	process_args(argc, argv);

	/* Set up line buffering. */
	setlinebuf(stdout);
	(void)puts("TrayInfo is initializing...");

	register_signal_handler();

#ifdef HAVE_LIBSSL
	/* Initialize SSL library. */
        SSL_library_init();
#endif /* HAVE_LIBSSL */

	/* Initialize dynamic library loader. */
	if (lt_dlinit() != 0) {
		show_error("unable to initialize dynamic loader.");
		return;
	}

	(void)lt_dlsetsearchpath(LIBDIR "/trayinfo/modules:./modules");

	/* Define the first configuration option handlers. */
	(void)register_config_handler("delay", config_delay);
	(void)register_config_handler("add", add_module);
	(void)register_config_handler("separator", set_separator);

	/* Initialize sources. */
	open_sources();
}

static void
run(void)
{
	double t0; /* Time of the last tick. */
	double t; /* Time till the next output. */

	t = delay;
	t0 = gettimeofdayd();
	while (quit_requested == 0) {
		double dt, now;
		fd_set rdfds;
		fd_set wrfds;
		int i, maxfd = -1;
		source_item_t *si;
		struct timeval tv;

/*		printf("%f\n", gettimeofdayd());*/

		FD_ZERO(&rdfds);
		FD_ZERO(&wrfds);
		si = first_source;
		while (si != NULL) {
			/* Try to reconnect disconnected sources. */
			if (si->fd == 0 && si->source.open != NULL &&
			    (time(NULL) >= si->last_retry + si->source.wait_retry)) {
				si->fd = si->source.open();
				si->last_retry = time(NULL);
			}

			/* Add active sources to select()'s rdfds list. */
			if (si->fd > 0) {
				FD_SET(si->fd, &rdfds);
				if (sendq_length(&(si->sendq)) > 0) {
					FD_SET(si->fd, &wrfds);
				}

				if (si->fd > maxfd)
					maxfd = si->fd;
			}

			si = si->next;
		}

		double2timeval(t, &tv);
#if 0
		show_error("select(%f);", t);
#endif
		i = select(maxfd + 1, &rdfds, &wrfds, NULL, &tv);
		if (i > 0) {
			si = first_source;
			while (si != NULL && i > 0) {
				if (FD_ISSET(si->fd, &rdfds)) {
					if (si->source.read(si->fd) <= 0) {
						si->source.close(si->fd);
						si->fd = 0;
					}
					--i;
				}
				if (FD_ISSET(si->fd, &wrfds)) {
					if (sendq_send(&(si->sendq), si->fd)
					    <= 0) {
						si->source.close(si->fd);
						si->fd = 0;
					}
					--i;
				}

				si = si->next;
			}
		}

		now = gettimeofdayd();
		dt = now - t0;
		if (dt < 0.0f) {
			dt = 0.0f;
		}
		t0 = now;
		t -= dt;
		if (t < 0.01f) {
			si = first_source;
			while (si != NULL) {
				(void)si->source.write(now, si->fd);
				si = si->next;
			}

			show_new_line();

			t += delay;
			if (t < -5.0 * delay) {
				t = -5.0 * delay;
			}
		}

		if (reload_requested != 0) {
			close_sources();
			open_sources();
			reload_requested = 0;
		}
	}
}

/**
 * Cleans up things before application terminates.
 */
static void
finalize(void)
{
	(void)puts("TrayInfo is exiting...");

	/* Close all sources */
	close_sources();

	/* Clean up default configuration option handlers. */
	(void)forget_config_handler("separator", set_separator);
	(void)forget_config_handler("add", add_module);
	(void)forget_config_handler("delay", config_delay);

	/* Close dynamic library loader. */
	(void)lt_dlsetsearchpath(""); /* libtool leak workaround */

	if (lt_dlexit() != 0) {
		show_error("Problems while closing dynamic library loader.");
	}

#ifdef HAVE_LIBSSL
	{
		STACK_OF(SSL_COMP) *ssl_comp_methods;
		int n;

		ssl_comp_methods = SSL_COMP_get_compression_methods();
		n = sk_SSL_COMP_num(ssl_comp_methods);

		/* Close SSL library. */
		ENGINE_cleanup();
		CRYPTO_cleanup_all_ex_data();
		ERR_free_strings();
		ERR_remove_state(0);
		EVP_cleanup();
		CONF_modules_unload(1);

		/* 
		 * A weak libssl leak workaround.
		 * This is not complete, dso freeup is missing, but it
		 * cannot be achieved from the outside of libssl.
		 */
		if (n > 0) {
			int j;

			for (j = 0; j < n; ++j) {
				SSL_COMP *c = sk_SSL_COMP_value(ssl_comp_methods, j);
				OPENSSL_free(c);
			}
		}
		if (ssl_comp_methods != NULL) {
			sk_SSL_COMP_free(ssl_comp_methods);
		}
	}

#endif /* HAVE_LIBSSL */
}

int
main(int argc, char *argv[])
{
	initialize(argc, argv);
	run();
	finalize();
	return 0;
}
